package pl.com.qapps.util.concurrent;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 
 * @author Jakub Dykowski
 * 
 */
public abstract class FutureWrapper<W, V> implements Future<V> {

	private final Lock lock = new ReentrantLock();

	private final Future<W> wrapped;

	private V result;
	private ExecutionException exception;
	private boolean hasResult;

	/**
	 * 
	 */
	public FutureWrapper(Future<W> wrapped) {
		if (wrapped == null)
			throw new NullPointerException();
		this.wrapped = wrapped;
	}

	@Override
	public final V get() throws InterruptedException, ExecutionException {
		lock.lock();
		try {
			if (hasResult)
				return getCached();
			W value;
			try {
				value = wrapped.get();
			} catch (ExecutionException e) {
				try {
					return wrapAndCache(absorbException(e.getCause()));
				} catch (Throwable parentException) {
					throw wrapAndCache(parentException);
				}
			}
			return wrapAndCache(value);
		} finally {
			lock.unlock();
		}
	}

	@Override
	public final V get(long timeout, TimeUnit unit)
			throws InterruptedException, ExecutionException, TimeoutException {
		long tryLockTime = System.currentTimeMillis();
		if (!lock.tryLock(timeout, unit))
			throw new TimeoutException();
		try {
			if (hasResult)
				return getCached();
			long remainingTimeout = TimeUnit.MILLISECONDS
					.convert(timeout, unit)
					- (System.currentTimeMillis() - tryLockTime);
			W value;
			try {
				value = wrapped.get(remainingTimeout, TimeUnit.MILLISECONDS);
			} catch (ExecutionException e) {
				try {
					return wrapAndCache(absorbException(e.getCause()));
				} catch (Throwable parentException) {
					throw wrapAndCache(parentException);
				}
			}
			return wrapAndCache(value);
		} finally {
			lock.unlock();
		}
	}

	@Override
	public final boolean isCancelled() {
		return wrapped.isCancelled();
	}

	@Override
	public final boolean isDone() {
		return wrapped.isDone();
	}

	@Override
	public final boolean cancel(boolean mayInterruptIfRunning) {
		return wrapped.cancel(mayInterruptIfRunning);
	}

	@Override
	public final boolean equals(Object obj) {
		return super.equals(obj);
	}

	@Override
	public final int hashCode() {
		return super.hashCode();
	}

	protected W absorbException(Throwable t) throws Throwable {
		throw t;
	}

	protected abstract V wrap(W wrapped);

	protected abstract Throwable wrapException(Throwable t);

	private V getCached() throws ExecutionException {
		if (exception != null)
			throw exception;
		return result;
	}

	private V wrapAndCache(W value) {
		hasResult = true;
		return result = wrap(value);
	}

	private ExecutionException wrapAndCache(Throwable t) {
		hasResult = true;
		return exception = new ExecutionException(wrapException(t));
	}
}
